/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2008 Sun Microsystems, Inc.  Portions Copyrighted 2008 Richard Schilling.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s): Richard Schilling (coderroadie@gmail.com)
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.imap.protocol;

import java.io.*;
import java.util.*;
import java.text.*;
import java.lang.reflect.*;

import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.search.*;

import com.sun.mail.util.*;
import com.sun.mail.iap.*;

import com.sun.mail.imap.ACL;
import com.sun.mail.imap.Rights;
import com.sun.mail.imap.AppendUID;

/**
 * This class extends the iap.Protocol object and implements IMAP
 * semantics. In general, there is a method corresponding to each
 * IMAP protocol command. The typical implementation issues the
 * appropriate protocol command, collects all responses, processes
 * those responses that are specific to this command and then
 * dispatches the rest (the unsolicited ones) to the dispatcher
 * using the <code>notifyResponseHandlers(r)</code>.
 * 
 * @author John Mani
 * @author Bill Shannon
 */

public class IMAPProtocol extends Protocol {

    private boolean connected = false;		// did constructor succeed?
    private boolean rev1 = false;		// REV1 server ?
    private boolean authenticated;		// authenticated?
    // WARNING: authenticated may be set to true in superclass
    // constructor, don't initialize it here.

    private Map capabilities;
    // WARNING: capabilities may be initialized as a result of superclass
    // constructor, don't initialize it here.
    private List authmechs;
    // WARNING: authmechs may be initialized as a result of superclass
    // constructor, don't initialize it here.
    private String[] searchCharsets; 	// array of search charsets

    private String name;
    private SaslAuthenticator saslAuthenticator;	// if SASL is being used

    private ByteArray ba;		// a buffer for fetchBody

    private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };

    /**
     * Constructor.
     * Opens a connection to the given host at given port.
     * 
     * @param host host to connect to
     * @param port portnumber to connect to
     * @param debug debug mode
     * @param props Properties object used by this protocol
     */
    public IMAPProtocol(String name, String host, int port, boolean debug, PrintStream out,
            Properties props, boolean isSSL) throws IOException, ProtocolException {
        super(host, port, debug, out, props, "mail." + name, isSSL);

        try {
            this.name = name;

            if (capabilities == null)
                capability();

            if (hasCapability("IMAP4rev1"))
                rev1 = true;

            searchCharsets = new String[2]; // 2, for now.
            searchCharsets[0] = "UTF-8";
            searchCharsets[1] = MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset());

            connected = true;	// must be last statement in constructor
        } finally {
            /*
             * If we get here because an exception was thrown, we need
             * to disconnect to avoid leaving a connected socket that
             * no one will be able to use because this object was never
             * completely constructed.
             */
            if (!connected)
                disconnect();
        }
    }

    /**
     * CAPABILITY command.
     * 
     * @see "RFC2060, section 6.1.1"
     */
    public void capability() throws ProtocolException {
        // Check CAPABILITY
        Response[] r = command("CAPABILITY", null);

        if (!r[r.length - 1].isOK())
            throw new ProtocolException(r[r.length - 1].toString());

        capabilities = new HashMap(10);
        authmechs = new ArrayList(5);
        for (int i = 0, len = r.length; i < len; i++) {
            if (!(r[i] instanceof IMAPResponse))
                continue;

            IMAPResponse ir = (IMAPResponse) r[i];

            // Handle *all* untagged CAPABILITY responses.
            // Though the spec seemingly states that only
            // one CAPABILITY response string is allowed (6.1.1),
            // some server vendors claim otherwise.
            if (ir.keyEquals("CAPABILITY"))
                parseCapabilities(ir);
        }
    }

    /**
     * If the response contains a CAPABILITY response code, extract
     * it and save the capabilities.
     */
    protected void setCapabilities(Response r) {
        byte b;
        while ((b = r.readByte()) > 0 && b != (byte) '[')
            ;
        if (b == 0)
            return;
        String s;
        s = r.readAtom();
        if (!s.equalsIgnoreCase("CAPABILITY"))
            return;
        capabilities = new HashMap(10);
        authmechs = new ArrayList(5);
        parseCapabilities(r);
    }

    /**
     * Parse the capabilities from a CAPABILITY response or from
     * a CAPABILITY response code attached to (e.g.) an OK response.
     */
    protected void parseCapabilities(Response r) {
        String s;
        while ((s = r.readAtom(']')) != null) {
            if (s.length() == 0) {
                if (r.peekByte() == (byte) ']')
                    break;
                /*
                 * Probably found something here that's not an atom.
                 * Rather than loop forever or fail completely, we'll
                 * try to skip this bogus capability. This is known
                 * to happen with:
                 * Netscape Messaging Server 4.03 (built Apr 27 1999)
                 * that returns:
                 * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ...
                 * The "*" in the middle of the capability list causes
                 * us to loop forever here.
                 */
                r.skipToken();
            } else {
                capabilities.put(s.toUpperCase(Locale.ENGLISH), s);
                if (s.regionMatches(true, 0, "AUTH=", 0, 5)) {
                    authmechs.add(s.substring(5));
                    if (debug)
                        out.println("IMAP DEBUG: AUTH: " + s.substring(5));
                }
            }
        }
    }

    /**
     * Check the greeting when first connecting; look for PREAUTH response.
     */
    protected void processGreeting(Response r) throws ProtocolException {
        super.processGreeting(r);	// check if it's BAD
        if (r.isOK()) {			// check if it's OK
            setCapabilities(r);
            return;
        }
        // only other choice is PREAUTH
        IMAPResponse ir = (IMAPResponse) r;
        if (ir.keyEquals("PREAUTH")) {
            authenticated = true;
            setCapabilities(r);
        } else
            throw new ConnectionException(this, r);
    }

    /**
     * Returns <code>true</code> if the connection has been authenticated,
     * either due to a successful login, or due to a PREAUTH greeting response.
     */
    public boolean isAuthenticated() {
        return authenticated;
    }

    /**
     * Returns <code>true</code> if this is a IMAP4rev1 server
     */
    public boolean isREV1() {
        return rev1;
    }

    /**
     * Returns whether this Protocol supports non-synchronizing literals.
     */
    protected boolean supportsNonSyncLiterals() {
        return hasCapability("LITERAL+");
    }

    /**
     * Read a response from the server.
     */
    public Response readResponse() throws IOException, ProtocolException {
        // assert Thread.holdsLock(this);
        // can't assert because it's called from constructor
        return IMAPResponse.readResponse(this);
    }

    /**
     * Check whether the given capability is supported by
     * this server. Returns <code>true</code> if so, otherwise
     * returns false.
     */
    public boolean hasCapability(String c) {
        return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Return the map of capabilities returned by the server.
     * 
     * @since JavaMail 1.4.1
     */
    public Map getCapabilities() {
        return capabilities;
    }

    /**
     * Close socket connection.
     * This method just makes the Protocol.disconnect() method
     * public.
     */
    public void disconnect() {
        super.disconnect();
        authenticated = false;	// just in case
    }

    /**
     * The NOOP command.
     * 
     * @see "RFC2060, section 6.1.2"
     */
    public void noop() throws ProtocolException {
        if (debug)
            out.println("IMAP DEBUG: IMAPProtocol noop");
        simpleCommand("NOOP", null);
    }

    /**
     * LOGOUT Command.
     * 
     * @see "RFC2060, section 6.1.3"
     */
    public void logout() throws ProtocolException {
        try {
            Response[] r = command("LOGOUT", null);

            authenticated = false;
            // dispatch any unsolicited responses.
            // NOTE that the BYE response is dispatched here as well
            notifyResponseHandlers(r);
        } finally {
            disconnect();
        }
    }

    /**
     * LOGIN Command.
     * 
     * @see "RFC2060, section 6.2.2"
     */
    public void login(String u, String p) throws ProtocolException {
        Argument args = new Argument();
        args.writeString(u);
        args.writeString(p);

        Response[] r = command("LOGIN", args);

        // dispatch untagged responses
        notifyResponseHandlers(r);

        // Handle result of this command
        handleResult(r[r.length - 1]);
        // If the response includes a CAPABILITY response code, process it
        setCapabilities(r[r.length - 1]);
        // if we get this far without an exception, we're authenticated
        authenticated = true;
    }

    /**
     * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme
     * 
     * @see "RFC2060, section 6.2.1"
     */
    public synchronized void authlogin(String u, String p) throws ProtocolException {
        Vector v = new Vector();
        String tag = null;
        Response r = null;
        boolean done = false;

        try {
            tag = writeCommand("AUTHENTICATE LOGIN", null);
        } catch (Exception ex) {
            // Convert this into a BYE response
            r = Response.byeResponse(ex);
            done = true;
        }

        OutputStream os = getOutputStream(); // stream to IMAP server

        /*
         * Wrap a BASE64Encoder around a ByteArrayOutputstream
         * to craft b64 encoded username and password strings
         * 
         * Note that the encoded bytes should be sent "as-is" to the
         * server, *not* as literals or quoted-strings.
         * 
         * Also note that unlike the B64 definition in MIME, CRLFs
         * should *not* be inserted during the encoding process. So, I
         * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine,
         * which should be sufficiently large !
         * 
         * Finally, format the line in a buffer so it can be sent as
         * a single packet, to avoid triggering a bug in SUN's SIMS 2.0
         * server caused by patch 105346.
         */

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);
        boolean first = true;

        while (!done) { // loop till we are done
            try {
                r = readResponse();
                if (r.isContinuation()) {
                    // Server challenge ..
                    String s;
                    if (first) { // Send encoded username
                        s = u;
                        first = false;
                    } else
                        // Send encoded password
                        s = p;

                    // obtain b64 encoded bytes
                    b64os.write(ASCIIUtility.getBytes(s));
                    b64os.flush(); 	// complete the encoding

                    bos.write(CRLF); 	// CRLF termination
                    os.write(bos.toByteArray()); // write out line
                    os.flush(); 	// flush the stream
                    bos.reset(); 	// reset buffer
                } else if (r.isTagged() && r.getTag().equals(tag))
                    // Ah, our tagged response
                    done = true;
                else if (r.isBYE()) // outta here
                    done = true;
                else
                    // hmm .. unsolicited response here ?!
                    v.addElement(r);
            } catch (Exception ioex) {
                // convert this into a BYE response
                r = Response.byeResponse(ioex);
                done = true;
            }
        }

        /*
         * Dispatch untagged responses.
         * NOTE: in our current upper level IMAP classes, we add the
         * responseHandler to the Protocol object only *after* the
         * connection has been authenticated. So, for now, the below
         * code really ends up being just a no-op.
         */
        Response[] responses = new Response[v.size()];
        v.copyInto(responses);
        notifyResponseHandlers(responses);

        // Handle the final OK, NO, BAD or BYE response
        handleResult(r);
        // If the response includes a CAPABILITY response code, process it
        setCapabilities(r);
        // if we get this far without an exception, we're authenticated
        authenticated = true;
    }

    /**
     * The AUTHENTICATE command with AUTH=PLAIN authentication scheme.
     * This is based heavly on the {@link #authlogin} method.
     * 
     * @param authzid the authorization id
     * @param u the username
     * @param p the password
     * @throws ProtocolException as thrown by {@link Protocol#handleResult}.
     * @see "RFC3501, section 6.2.2"
     * @see "RFC2595, section 6"
     * @since JavaMail 1.3.2
     */
    public synchronized void authplain(String authzid, String u, String p) throws ProtocolException {
        Vector v = new Vector();
        String tag = null;
        Response r = null;
        boolean done = false;

        try {
            tag = writeCommand("AUTHENTICATE PLAIN", null);
        } catch (Exception ex) {
            // Convert this into a BYE response
            r = Response.byeResponse(ex);
            done = true;
        }

        OutputStream os = getOutputStream(); // stream to IMAP server

        /*
         * Wrap a BASE64Encoder around a ByteArrayOutputstream
         * to craft b64 encoded username and password strings
         * 
         * Note that the encoded bytes should be sent "as-is" to the
         * server, *not* as literals or quoted-strings.
         * 
         * Also note that unlike the B64 definition in MIME, CRLFs
         * should *not* be inserted during the encoding process. So, I
         * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine,
         * which should be sufficiently large !
         * 
         * Finally, format the line in a buffer so it can be sent as
         * a single packet, to avoid triggering a bug in SUN's SIMS 2.0
         * server caused by patch 105346.
         */

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);

        while (!done) { // loop till we are done
            try {
                r = readResponse();
                if (r.isContinuation()) {
                    // Server challenge ..
                    final String nullByte = "\0";
                    String s = authzid + nullByte + u + nullByte + p;

                    // obtain b64 encoded bytes
                    b64os.write(ASCIIUtility.getBytes(s));
                    b64os.flush(); 	// complete the encoding

                    bos.write(CRLF); 	// CRLF termination
                    os.write(bos.toByteArray()); // write out line
                    os.flush(); 	// flush the stream
                    bos.reset(); 	// reset buffer
                } else if (r.isTagged() && r.getTag().equals(tag))
                    // Ah, our tagged response
                    done = true;
                else if (r.isBYE()) // outta here
                    done = true;
                else
                    // hmm .. unsolicited response here ?!
                    v.addElement(r);
            } catch (Exception ioex) {
                // convert this into a BYE response
                r = Response.byeResponse(ioex);
                done = true;
            }
        }

        /*
         * Dispatch untagged responses.
         * NOTE: in our current upper level IMAP classes, we add the
         * responseHandler to the Protocol object only *after* the
         * connection has been authenticated. So, for now, the below
         * code really ends up being just a no-op.
         */
        Response[] responses = new Response[v.size()];
        v.copyInto(responses);
        notifyResponseHandlers(responses);

        // Handle the final OK, NO, BAD or BYE response
        handleResult(r);
        // If the response includes a CAPABILITY response code, process it
        setCapabilities(r);
        // if we get this far without an exception, we're authenticated
        authenticated = true;
    }

    /**
     * SASL-based login.
     */
    public void sasllogin(String[] allowed, String realm, String authzid, String u, String p)
            throws ProtocolException {
        if (saslAuthenticator == null) {
            try {
                Class sac = Class.forName("com.sun.mail.imap.protocol.IMAPSaslAuthenticator");
                Constructor c = sac
                        .getConstructor(new Class[] { IMAPProtocol.class, String.class, Properties.class, Boolean.TYPE, PrintStream.class, String.class });
                saslAuthenticator = (SaslAuthenticator) c
                        .newInstance(new Object[] { this, name, props, debug ? Boolean.TRUE
                                : Boolean.FALSE, out, host });
            } catch (Exception ex) {
                if (debug)
                    out.println("IMAP DEBUG: Can't load SASL authenticator: " + ex);
                // probably because we're running on a system without SASL
                return;	// not authenticated, try without SASL
            }
        }

        // were any allowed mechanisms specified?
        List v;
        if (allowed != null && allowed.length > 0) {
            // remove anything not supported by the server
            v = new ArrayList(allowed.length);
            for (int i = 0; i < allowed.length; i++)
                if (authmechs.contains(allowed[i]))	// XXX - case must match
                    v.add(allowed[i]);
        } else {
            // everything is allowed
            v = authmechs;
        }
        String[] mechs = (String[]) v.toArray(new String[v.size()]);
        if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p))
            authenticated = true;
    }

    // XXX - for IMAPSaslAuthenticator access to protected method
    OutputStream getIMAPOutputStream() {
        return getOutputStream();
    }

    /**
     * PROXYAUTH Command.
     * 
     * @see "Netscape/iPlanet/SunONE Messaging Server extension"
     */
    public void proxyauth(String u) throws ProtocolException {
        Argument args = new Argument();
        args.writeString(u);

        simpleCommand("PROXYAUTH", args);
    }

    /**
     * STARTTLS Command.
     * 
     * @see "RFC3501, section 6.2.1"
     */
    public void startTLS() throws ProtocolException {
        try {
            super.startTLS("STARTTLS");
        } catch (ProtocolException pex) {
            // ProtocolException just means the command wasn't recognized,
            // or failed. This should never happen if we check the
            // CAPABILITY first.
            throw pex;
        } catch (Exception ex) {
            // any other exception means we have to shut down the connection
            // generate an artificial BYE response and disconnect
            Response[] r = { Response.byeResponse(ex) };
            notifyResponseHandlers(r);
            disconnect();
        }
    }

    /**
     * SELECT Command.
     * 
     * @see "RFC2060, section 6.3.1"
     */
    public MailboxInfo select(String mbox) throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Response[] r = command("SELECT", args);

        // Note that MailboxInfo also removes those responses
        // it knows about
        MailboxInfo minfo = new MailboxInfo(r);

        // dispatch any remaining untagged responses
        notifyResponseHandlers(r);

        Response response = r[r.length - 1];

        if (response.isOK()) { // command succesful
            if (response.toString().indexOf("READ-ONLY") != -1)
                minfo.mode = Folder.READ_ONLY;
            else
                minfo.mode = Folder.READ_WRITE;
        }

        handleResult(response);
        return minfo;
    }

    /**
     * EXAMINE Command.
     * 
     * @see "RFC2060, section 6.3.2"
     */
    public MailboxInfo examine(String mbox) throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Response[] r = command("EXAMINE", args);

        // Note that MailboxInfo also removes those responses
        // it knows about
        MailboxInfo minfo = new MailboxInfo(r);
        minfo.mode = Folder.READ_ONLY; // Obviously

        // dispatch any remaining untagged responses
        notifyResponseHandlers(r);

        handleResult(r[r.length - 1]);
        return minfo;
    }

    /**
     * STATUS Command.
     * 
     * @see "RFC2060, section 6.3.10"
     */
    public Status status(String mbox, String[] items) throws ProtocolException {
        if (!isREV1() && !hasCapability("IMAP4SUNVERSION"))
            // STATUS is rev1 only, however the non-rev1 SIMS2.0
            // does support this.
            throw new BadCommandException("STATUS not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Argument itemArgs = new Argument();
        if (items == null)
            items = Status.standardItems;

        for (int i = 0, len = items.length; i < len; i++)
            itemArgs.writeAtom(items[i]);
        args.writeArgument(itemArgs);

        Response[] r = command("STATUS", args);

        Status status = null;
        Response response = r[r.length - 1];

        // Grab all STATUS responses
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("STATUS")) {
                    if (status == null)
                        status = new Status(ir);
                    else
                        // collect 'em all
                        Status.add(status, new Status(ir));
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        return status;
    }

    /**
     * CREATE Command.
     * 
     * @see "RFC2060, section 6.3.3"
     */
    public void create(String mbox) throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        simpleCommand("CREATE", args);
    }

    /**
     * DELETE Command.
     * 
     * @see "RFC2060, section 6.3.4"
     */
    public void delete(String mbox) throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        simpleCommand("DELETE", args);
    }

    /**
     * RENAME Command.
     * 
     * @see "RFC2060, section 6.3.5"
     */
    public void rename(String o, String n) throws ProtocolException {
        // encode the mbox as per RFC2060
        o = BASE64MailboxEncoder.encode(o);
        n = BASE64MailboxEncoder.encode(n);

        Argument args = new Argument();
        args.writeString(o);
        args.writeString(n);

        simpleCommand("RENAME", args);
    }

    /**
     * SUBSCRIBE Command.
     * 
     * @see "RFC2060, section 6.3.6"
     */
    public void subscribe(String mbox) throws ProtocolException {
        Argument args = new Argument();
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);
        args.writeString(mbox);

        simpleCommand("SUBSCRIBE", args);
    }

    /**
     * UNSUBSCRIBE Command.
     * 
     * @see "RFC2060, section 6.3.7"
     */
    public void unsubscribe(String mbox) throws ProtocolException {
        Argument args = new Argument();
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);
        args.writeString(mbox);

        simpleCommand("UNSUBSCRIBE", args);
    }

    /**
     * LIST Command.
     * 
     * @see "RFC2060, section 6.3.8"
     */
    public ListInfo[] list(String ref, String pattern) throws ProtocolException {
        return doList("LIST", ref, pattern);
    }

    /**
     * LSUB Command.
     * 
     * @see "RFC2060, section 6.3.9"
     */
    public ListInfo[] lsub(String ref, String pattern) throws ProtocolException {
        return doList("LSUB", ref, pattern);
    }

    private ListInfo[] doList(String cmd, String ref, String pat) throws ProtocolException {
        // encode the mbox as per RFC2060
        ref = BASE64MailboxEncoder.encode(ref);
        pat = BASE64MailboxEncoder.encode(pat);

        Argument args = new Argument();
        args.writeString(ref);
        args.writeString(pat);

        Response[] r = command(cmd, args);

        ListInfo[] linfo = null;
        Response response = r[r.length - 1];

        if (response.isOK()) { // command succesful
            Vector v = new Vector(1);
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals(cmd)) {
                    v.addElement(new ListInfo(ir));
                    r[i] = null;
                }
            }
            if (v.size() > 0) {
                linfo = new ListInfo[v.size()];
                v.copyInto(linfo);
            }
        }

        // Dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        return linfo;
    }

    /**
     * APPEND Command.
     * 
     * @see "RFC2060, section 6.3.11"
     */
    public void append(String mbox, Flags f, Date d, Literal data) throws ProtocolException {
        appenduid(mbox, f, d, data, false);	// ignore return value
    }

    /**
     * APPEND Command, return uid from APPENDUID response code.
     * 
     * @see "RFC2060, section 6.3.11"
     */
    public AppendUID appenduid(String mbox, Flags f, Date d, Literal data) throws ProtocolException {
        return appenduid(mbox, f, d, data, true);
    }

    public AppendUID appenduid(String mbox, Flags f, Date d, Literal data, boolean uid)
            throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        if (f != null) { // set Flags in appended message
            // can't set the \Recent flag in APPEND
            if (f.contains(Flags.Flag.RECENT)) {
                f = new Flags(f);		// copy, don't modify orig
                f.remove(Flags.Flag.RECENT);	// remove RECENT from copy
            }

            /*
             * HACK ALERT: We want the flag_list to be written out
             * without any checking/processing of the bytes in it. If
             * I use writeString(), the flag_list will end up being
             * quoted since it contains "illegal" characters. So I
             * am depending on implementation knowledge that writeAtom()
             * does not do any checking/processing - it just writes out
             * the bytes. What we really need is a writeFoo() that just
             * dumps out its argument.
             */
            args.writeAtom(createFlagList(f));
        }
        if (d != null) // set INTERNALDATE in appended message
            args.writeString(INTERNALDATE.format(d));

        args.writeBytes(data);

        Response[] r = command("APPEND", args);

        // dispatch untagged responses
        notifyResponseHandlers(r);

        // Handle result of this command
        handleResult(r[r.length - 1]);

        if (uid)
            return getAppendUID(r[r.length - 1]);
        else
            return null;
    }

    /**
     * If the response contains an APPENDUID response code, extract
     * it and return an AppendUID object with the information.
     */
    private AppendUID getAppendUID(Response r) {
        if (!r.isOK())
            return null;
        byte b;
        while ((b = r.readByte()) > 0 && b != (byte) '[')
            ;
        if (b == 0)
            return null;
        String s;
        s = r.readAtom();
        if (!s.equalsIgnoreCase("APPENDUID"))
            return null;

        long uidvalidity = r.readLong();
        long uid = r.readLong();
        return new AppendUID(uidvalidity, uid);
    }

    /**
     * CHECK Command.
     * 
     * @see "RFC2060, section 6.4.1"
     */
    public void check() throws ProtocolException {
        simpleCommand("CHECK", null);
    }

    /**
     * CLOSE Command.
     * 
     * @see "RFC2060, section 6.4.2"
     */
    public void close() throws ProtocolException {
        simpleCommand("CLOSE", null);
    }

    /**
     * EXPUNGE Command.
     * 
     * @see "RFC2060, section 6.4.3"
     */
    public void expunge() throws ProtocolException {
        simpleCommand("EXPUNGE", null);
    }

    /**
     * UID EXPUNGE Command.
     * 
     * @see "RFC2359, section 4.1"
     */
    public void uidexpunge(UIDSet[] set) throws ProtocolException {
        if (!hasCapability("UIDPLUS"))
            throw new BadCommandException("UID EXPUNGE not supported");
        simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null);
    }

    /**
     * Fetch the BODYSTRUCTURE of the specified message.
     */
    public BODYSTRUCTURE fetchBodyStructure(int msgno) throws ProtocolException {
        Response[] r = fetch(msgno, "BODYSTRUCTURE");
        notifyResponseHandlers(r);

        Response response = r[r.length - 1];
        if (response.isOK())
            return (BODYSTRUCTURE) FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class);
        else if (response.isNO())
            return null;
        else {
            handleResult(response);
            return null;
        }
    }

    /**
     * Fetch given BODY section, without marking the message
     * as SEEN.
     */
    public BODY peekBody(int msgno, String section) throws ProtocolException {
        return fetchBody(msgno, section, true);
    }

    /**
     * Fetch given BODY section.
     */
    public BODY fetchBody(int msgno, String section) throws ProtocolException {
        return fetchBody(msgno, section, false);
    }

    protected BODY fetchBody(int msgno, String section, boolean peek) throws ProtocolException {
        Response[] r;

        if (peek)
            r = fetch(msgno, "BODY.PEEK[" + (section == null ? "]" : section + "]"));
        else
            r = fetch(msgno, "BODY[" + (section == null ? "]" : section + "]"));

        notifyResponseHandlers(r);

        Response response = r[r.length - 1];
        if (response.isOK())
            return (BODY) FetchResponse.getItem(r, msgno, BODY.class);
        else if (response.isNO())
            return null;
        else {
            handleResult(response);
            return null;
        }
    }

    /**
     * Partial FETCH of given BODY section, without setting SEEN flag.
     */
    public BODY peekBody(int msgno, String section, int start, int size) throws ProtocolException {
        return fetchBody(msgno, section, start, size, true, null);
    }

    /**
     * Partial FETCH of given BODY section.
     */
    public BODY fetchBody(int msgno, String section, int start, int size) throws ProtocolException {
        return fetchBody(msgno, section, start, size, false, null);
    }

    /**
     * Partial FETCH of given BODY section, without setting SEEN flag.
     */
    public BODY peekBody(int msgno, String section, int start, int size, ByteArray ba)
            throws ProtocolException {
        return fetchBody(msgno, section, start, size, true, ba);
    }

    /**
     * Partial FETCH of given BODY section.
     */
    public BODY fetchBody(int msgno, String section, int start, int size, ByteArray ba)
            throws ProtocolException {
        return fetchBody(msgno, section, start, size, false, ba);
    }

    protected BODY fetchBody(int msgno, String section, int start, int size, boolean peek,
            ByteArray ba) throws ProtocolException {
        this.ba = ba;	// save for later use by getResponseBuffer
        Response[] r = fetch(msgno, (peek ? "BODY.PEEK[" : "BODY[")
                + (section == null ? "]<" : (section + "]<")) + String.valueOf(start) + "."
                + String.valueOf(size) + ">");

        notifyResponseHandlers(r);

        Response response = r[r.length - 1];
        if (response.isOK())
            return (BODY) FetchResponse.getItem(r, msgno, BODY.class);
        else if (response.isNO())
            return null;
        else {
            handleResult(response);
            return null;
        }
    }

    /**
     * Return a buffer to read a response into.
     * The buffer is provided by fetchBody and is
     * used only once.
     */
    protected ByteArray getResponseBuffer() {
        ByteArray ret = ba;
        ba = null;
        return ret;
    }

    /**
     * Fetch the specified RFC822 Data item. 'what' names
     * the item to be fetched. 'what' can be <code>null</code> to fetch the
     * whole message.
     */
    public RFC822DATA fetchRFC822(int msgno, String what) throws ProtocolException {
        Response[] r = fetch(msgno, what == null ? "RFC822" : "RFC822." + what);

        // dispatch untagged responses
        notifyResponseHandlers(r);

        Response response = r[r.length - 1];
        if (response.isOK())
            return (RFC822DATA) FetchResponse.getItem(r, msgno, RFC822DATA.class);
        else if (response.isNO())
            return null;
        else {
            handleResult(response);
            return null;
        }
    }

    /**
     * Fetch the FLAGS for the given message.
     */
    public Flags fetchFlags(int msgno) throws ProtocolException {
        Flags flags = null;
        Response[] r = fetch(msgno, "FLAGS");

        // Search for our FLAGS response
        for (int i = 0, len = r.length; i < len; i++) {
            if (r[i] == null || !(r[i] instanceof FetchResponse)
                    || ((FetchResponse) r[i]).getNumber() != msgno)
                continue;

            FetchResponse fr = (FetchResponse) r[i];
            if ((flags = (Flags) fr.getItem(Flags.class)) != null) {
                r[i] = null; // remove this response
                break;
            }
        }

        // dispatch untagged responses
        notifyResponseHandlers(r);
        handleResult(r[r.length - 1]);
        return flags;
    }

    /**
     * Fetch the IMAP UID for the given message.
     */
    public UID fetchUID(int msgno) throws ProtocolException {
        Response[] r = fetch(msgno, "UID");

        // dispatch untagged responses
        notifyResponseHandlers(r);

        Response response = r[r.length - 1];
        if (response.isOK())
            return (UID) FetchResponse.getItem(r, msgno, UID.class);
        else if (response.isNO()) // XXX: Issue NOOP ?
            return null;
        else {
            handleResult(response);
            return null; // NOTREACHED
        }
    }

    /**
     * Get the sequence number for the given UID. A UID object
     * containing the sequence number is returned. If the given UID
     * is invalid, <code>null</code> is returned.
     */
    public UID fetchSequenceNumber(long uid) throws ProtocolException {
        UID u = null;
        Response[] r = fetch(String.valueOf(uid), "UID", true);

        for (int i = 0, len = r.length; i < len; i++) {
            if (r[i] == null || !(r[i] instanceof FetchResponse))
                continue;

            FetchResponse fr = (FetchResponse) r[i];
            if ((u = (UID) fr.getItem(UID.class)) != null) {
                if (u.uid == uid) // this is the one we want
                    break;
                else
                    u = null;
            }
        }

        notifyResponseHandlers(r);
        handleResult(r[r.length - 1]);
        return u;
    }

    /**
     * Get the sequence numbers for UIDs ranging from start till end.
     * UID objects that contain the sequence numbers are returned.
     * If no UIDs in the given range are found, an empty array is returned.
     */
    public UID[] fetchSequenceNumbers(long start, long end) throws ProtocolException {
        Response[] r = fetch(
                String.valueOf(start) + ":"
                        + (end == UIDFolder.LASTUID ? "*" : String.valueOf(end)), "UID", true);

        UID u;
        Vector v = new Vector();
        for (int i = 0, len = r.length; i < len; i++) {
            if (r[i] == null || !(r[i] instanceof FetchResponse))
                continue;

            FetchResponse fr = (FetchResponse) r[i];
            if ((u = (UID) fr.getItem(UID.class)) != null)
                v.addElement(u);
        }

        notifyResponseHandlers(r);
        handleResult(r[r.length - 1]);

        UID[] ua = new UID[v.size()];
        v.copyInto(ua);
        return ua;
    }

    /**
     * Get the sequence numbers for UIDs ranging from start till end.
     * UID objects that contain the sequence numbers are returned.
     * If no UIDs in the given range are found, an empty array is returned.
     */
    public UID[] fetchSequenceNumbers(long[] uids) throws ProtocolException {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < uids.length; i++) {
            if (i > 0)
                sb.append(",");
            sb.append(String.valueOf(uids[i]));
        }

        Response[] r = fetch(sb.toString(), "UID", true);

        UID u;
        Vector v = new Vector();
        for (int i = 0, len = r.length; i < len; i++) {
            if (r[i] == null || !(r[i] instanceof FetchResponse))
                continue;

            FetchResponse fr = (FetchResponse) r[i];
            if ((u = (UID) fr.getItem(UID.class)) != null)
                v.addElement(u);
        }

        notifyResponseHandlers(r);
        handleResult(r[r.length - 1]);

        UID[] ua = new UID[v.size()];
        v.copyInto(ua);
        return ua;
    }

    public Response[] fetch(MessageSet[] msgsets, String what) throws ProtocolException {
        return fetch(MessageSet.toString(msgsets), what, false);
    }

    public Response[] fetch(int start, int end, String what) throws ProtocolException {
        return fetch(String.valueOf(start) + ":" + String.valueOf(end), what, false);
    }

    public Response[] fetch(int msg, String what) throws ProtocolException {
        return fetch(String.valueOf(msg), what, false);
    }

    private Response[] fetch(String msgSequence, String what, boolean uid) throws ProtocolException {
        if (uid)
            return command("UID FETCH " + msgSequence + " (" + what + ")", null);
        else
            return command("FETCH " + msgSequence + " (" + what + ")", null);
    }

    /**
     * COPY command.
     */
    public void copy(MessageSet[] msgsets, String mbox) throws ProtocolException {
        copy(MessageSet.toString(msgsets), mbox);
    }

    public void copy(int start, int end, String mbox) throws ProtocolException {
        copy(String.valueOf(start) + ":" + String.valueOf(end), mbox);
    }

    private void copy(String msgSequence, String mbox) throws ProtocolException {
        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeAtom(msgSequence);
        args.writeString(mbox);

        simpleCommand("COPY", args);
    }

    public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) throws ProtocolException {
        storeFlags(MessageSet.toString(msgsets), flags, set);
    }

    public void storeFlags(int start, int end, Flags flags, boolean set) throws ProtocolException {
        storeFlags(String.valueOf(start) + ":" + String.valueOf(end), flags, set);
    }

    /**
     * Set the specified flags on this message.
     * <p>
     */
    public void storeFlags(int msg, Flags flags, boolean set) throws ProtocolException {
        storeFlags(String.valueOf(msg), flags, set);
    }

    private void storeFlags(String msgset, Flags flags, boolean set) throws ProtocolException {
        Response[] r;
        if (set)
            r = command("STORE " + msgset + " +FLAGS " + createFlagList(flags), null);
        else
            r = command("STORE " + msgset + " -FLAGS " + createFlagList(flags), null);

        // Dispatch untagged responses
        notifyResponseHandlers(r);
        handleResult(r[r.length - 1]);
    }

    /**
     * Creates an IMAP flag_list from the given Flags object.
     */
    private String createFlagList(Flags flags) {
        StringBuffer sb = new StringBuffer();
        sb.append("("); // start of flag_list

        Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags
        boolean first = true;
        for (int i = 0; i < sf.length; i++) {
            String s;
            Flags.Flag f = sf[i];
            if (f == Flags.Flag.ANSWERED)
                s = "\\Answered";
            else if (f == Flags.Flag.DELETED)
                s = "\\Deleted";
            else if (f == Flags.Flag.DRAFT)
                s = "\\Draft";
            else if (f == Flags.Flag.FLAGGED)
                s = "\\Flagged";
            else if (f == Flags.Flag.RECENT)
                s = "\\Recent";
            else if (f == Flags.Flag.SEEN)
                s = "\\Seen";
            else
                continue;	// skip it
            if (first)
                first = false;
            else
                sb.append(' ');
            sb.append(s);
        }

        String[] uf = flags.getUserFlags(); // get the user flag strings
        for (int i = 0; i < uf.length; i++) {
            if (first)
                first = false;
            else
                sb.append(' ');
            sb.append(uf[i]);
        }

        sb.append(")"); // terminate flag_list
        return sb.toString();
    }

    /**
     * Issue the given search criterion on the specified message sets.
     * Returns array of matching sequence numbers. An empty array
     * is returned if no matches are found.
     * 
     * @param msgsets array of MessageSets
     * @param term SearchTerm
     * @return array of matching sequence numbers.
     */
    public int[] search(MessageSet[] msgsets, SearchTerm term) throws ProtocolException,
            SearchException {
        return search(MessageSet.toString(msgsets), term);
    }

    /**
     * Issue the given search criterion on all messages in this folder.
     * Returns array of matching sequence numbers. An empty array
     * is returned if no matches are found.
     * 
     * @param term SearchTerm
     * @return array of matching sequence numbers.
     */
    public int[] search(SearchTerm term) throws ProtocolException, SearchException {
        return search("ALL", term);
    }

    /*
     * Apply the given SearchTerm on the specified sequence.
     * Returns array of matching sequence numbers. Note that an empty
     * array is returned for no matches.
     */
    private int[] search(String msgSequence, SearchTerm term) throws ProtocolException,
            SearchException {
        // Check if the search "text" terms contain only ASCII chars
        if (SearchSequence.isAscii(term)) {
            try {
                return issueSearch(msgSequence, term, null);
            } catch (IOException ioex) { /* will not happen */}
        }

        /*
         * The search "text" terms do contain non-ASCII chars. We need to
         * use SEARCH CHARSET <charset> ...
         * The charsets we try to use are UTF-8 and the locale's
         * default charset. If the server supports UTF-8, great,
         * always use it. Else we try to use the default charset.
         */

        // Cycle thru the list of charsets
        for (int i = 0; i < searchCharsets.length; i++) {
            if (searchCharsets[i] == null)
                continue;

            try {
                return issueSearch(msgSequence, term, searchCharsets[i]);
            } catch (CommandFailedException cfx) {
                /*
                 * Server returned NO. For now, I'll just assume that
                 * this indicates that this charset is unsupported.
                 * We can check the BADCHARSET response code once
                 * that's spec'd into the IMAP RFC ..
                 */
                searchCharsets[i] = null;
                continue;
            } catch (IOException ioex) {
                /* Charset conversion failed. Try the next one */
                continue;
            } catch (ProtocolException pex) {
                throw pex;
            } catch (SearchException sex) {
                throw sex;
            }
        }

        // No luck.
        throw new SearchException("Search failed");
    }

    /*
     * Apply the given SearchTerm on the specified sequence, using the
     * given charset. <p>
     * Returns array of matching sequence numbers. Note that an empty
     * array is returned for no matches.
     */
    private int[] issueSearch(String msgSequence, SearchTerm term, String charset)
            throws ProtocolException, SearchException, IOException {

        // Generate a search-sequence with the given charset
        Argument args = SearchSequence.generateSequence(term,
                charset == null ? null : MimeUtility.javaCharset(charset));
        args.writeAtom(msgSequence);

        Response[] r;

        if (charset == null) // text is all US-ASCII
            r = command("SEARCH", args);
        else
            r = command("SEARCH CHARSET " + charset, args);

        Response response = r[r.length - 1];
        int[] matches = null;

        // Grab all SEARCH responses
        if (response.isOK()) { // command succesful
            Vector v = new Vector();
            int num;
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                // There *will* be one SEARCH response.
                if (ir.keyEquals("SEARCH")) {
                    while ((num = ir.readNumber()) != -1)
                        v.addElement(new Integer(num));
                    r[i] = null;
                }
            }

            // Copy the vector into 'matches'
            int vsize = v.size();
            matches = new int[vsize];
            for (int i = 0; i < vsize; i++)
                matches[i] = ((Integer) v.elementAt(i)).intValue();
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        return matches;
    }

    /**
     * NAMESPACE Command.
     * 
     * @see "RFC2342"
     */
    public Namespaces namespace() throws ProtocolException {
        if (!hasCapability("NAMESPACE"))
            throw new BadCommandException("NAMESPACE not supported");

        Response[] r = command("NAMESPACE", null);

        Namespaces namespace = null;
        Response response = r[r.length - 1];

        // Grab NAMESPACE response
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("NAMESPACE")) {
                    if (namespace == null)
                        namespace = new Namespaces(ir);
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        return namespace;
    }

    /**
     * GETQUOTAROOT Command.
     * Returns an array of Quota objects, representing the quotas
     * for this mailbox and, indirectly, the quotaroots for this
     * mailbox.
     * 
     * @see "RFC2087"
     */
    public Quota[] getQuotaRoot(String mbox) throws ProtocolException {
        if (!hasCapability("QUOTA"))
            throw new BadCommandException("GETQUOTAROOT not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Response[] r = command("GETQUOTAROOT", args);

        Response response = r[r.length - 1];

        Hashtable tab = new Hashtable();

        // Grab all QUOTAROOT and QUOTA responses
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("QUOTAROOT")) {
                    // quotaroot_response
                    // ::= "QUOTAROOT" SP astring *(SP astring)

                    // read name of mailbox and throw away
                    ir.readAtomString();
                    // for each quotaroot add a placeholder quota
                    String root = null;
                    while ((root = ir.readAtomString()) != null)
                        tab.put(root, new Quota(root));
                    r[i] = null;
                } else if (ir.keyEquals("QUOTA")) {
                    Quota quota = parseQuota(ir);
                    Quota q = (Quota) tab.get(quota.quotaRoot);
                    if (q != null && q.resources != null) {
                        // XXX - should merge resources
                    }
                    tab.put(quota.quotaRoot, quota);
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);

        Quota[] qa = new Quota[tab.size()];
        Enumeration e = tab.elements();
        for (int i = 0; e.hasMoreElements(); i++)
            qa[i] = (Quota) e.nextElement();
        return qa;
    }

    /**
     * GETQUOTA Command.
     * Returns an array of Quota objects, representing the quotas
     * for this quotaroot.
     * 
     * @see "RFC2087"
     */
    public Quota[] getQuota(String root) throws ProtocolException {
        if (!hasCapability("QUOTA"))
            throw new BadCommandException("QUOTA not supported");

        Argument args = new Argument();
        args.writeString(root);

        Response[] r = command("GETQUOTA", args);

        Quota quota = null;
        Vector v = new Vector();
        Response response = r[r.length - 1];

        // Grab all QUOTA responses
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("QUOTA")) {
                    quota = parseQuota(ir);
                    v.addElement(quota);
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        Quota[] qa = new Quota[v.size()];
        v.copyInto(qa);
        return qa;
    }

    /**
     * SETQUOTA Command.
     * Set the indicated quota on the corresponding quotaroot.
     * 
     * @see "RFC2087"
     */
    public void setQuota(Quota quota) throws ProtocolException {
        if (!hasCapability("QUOTA"))
            throw new BadCommandException("QUOTA not supported");

        Argument args = new Argument();
        args.writeString(quota.quotaRoot);
        Argument qargs = new Argument();
        if (quota.resources != null) {
            for (int i = 0; i < quota.resources.length; i++) {
                qargs.writeAtom(quota.resources[i].name);
                qargs.writeNumber(quota.resources[i].limit);
            }
        }
        args.writeArgument(qargs);

        Response[] r = command("SETQUOTA", args);
        Response response = r[r.length - 1];

        // XXX - It's not clear from the RFC whether the SETQUOTA command
        // will provoke untagged QUOTA responses. If it does, perhaps
        // we should grab them here and return them?

        /*
         * Quota quota = null;
         * Vector v = new Vector();
         * 
         * // Grab all QUOTA responses
         * if (response.isOK()) { // command succesful
         * for (int i = 0, len = r.length; i < len; i++) {
         * if (!(r[i] instanceof IMAPResponse))
         * continue;
         * 
         * IMAPResponse ir = (IMAPResponse)r[i];
         * if (ir.keyEquals("QUOTA")) {
         * quota = parseQuota(ir);
         * v.addElement(quota);
         * r[i] = null;
         * }
         * }
         * }
         */

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        /*
         * Quota[] qa = new Quota[v.size()];
         * v.copyInto(qa);
         * return qa;
         */
    }

    /**
     * Parse a QUOTA response.
     */
    private Quota parseQuota(Response r) throws ParsingException {
        // quota_response ::= "QUOTA" SP astring SP quota_list
        String quotaRoot = r.readAtomString();	// quotaroot ::= astring
        Quota q = new Quota(quotaRoot);
        r.skipSpaces();
        // quota_list ::= "(" #quota_resource ")"
        if (r.readByte() != '(')
            throw new ParsingException("parse error in QUOTA");

        Vector v = new Vector();
        while (r.peekByte() != ')') {
            // quota_resource ::= atom SP number SP number
            String name = r.readAtom();
            if (name != null) {
                long usage = r.readLong();
                long limit = r.readLong();
                Quota.Resource res = new Quota.Resource(name, usage, limit);
                v.addElement(res);
            }
        }
        r.readByte();
        q.resources = new Quota.Resource[v.size()];
        v.copyInto(q.resources);
        return q;
    }

    /**
     * SETACL Command.
     * 
     * @see "RFC2086"
     */
    public void setACL(String mbox, char modifier, ACL acl) throws ProtocolException {
        if (!hasCapability("ACL"))
            throw new BadCommandException("ACL not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);
        args.writeString(acl.getName());
        String rights = acl.getRights().toString();
        if (modifier == '+' || modifier == '-')
            rights = modifier + rights;
        args.writeString(rights);

        Response[] r = command("SETACL", args);
        Response response = r[r.length - 1];

        // dispatch untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
    }

    /**
     * DELETEACL Command.
     * 
     * @see "RFC2086"
     */
    public void deleteACL(String mbox, String user) throws ProtocolException {
        if (!hasCapability("ACL"))
            throw new BadCommandException("ACL not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);
        args.writeString(user);

        Response[] r = command("DELETEACL", args);
        Response response = r[r.length - 1];

        // dispatch untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
    }

    /**
     * GETACL Command.
     * 
     * @see "RFC2086"
     */
    public ACL[] getACL(String mbox) throws ProtocolException {
        if (!hasCapability("ACL"))
            throw new BadCommandException("ACL not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Response[] r = command("GETACL", args);
        Response response = r[r.length - 1];

        // Grab all ACL responses
        Vector v = new Vector();
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("ACL")) {
                    // acl_data ::= "ACL" SPACE mailbox
                    // *(SPACE identifier SPACE rights)
                    // read name of mailbox and throw away
                    ir.readAtomString();
                    String name = null;
                    while ((name = ir.readAtomString()) != null) {
                        String rights = ir.readAtomString();
                        if (rights == null)
                            break;
                        ACL acl = new ACL(name, new Rights(rights));
                        v.addElement(acl);
                    }
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        ACL[] aa = new ACL[v.size()];
        v.copyInto(aa);
        return aa;
    }

    /**
     * LISTRIGHTS Command.
     * 
     * @see "RFC2086"
     */
    public Rights[] listRights(String mbox, String user) throws ProtocolException {
        if (!hasCapability("ACL"))
            throw new BadCommandException("ACL not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);
        args.writeString(user);

        Response[] r = command("LISTRIGHTS", args);
        Response response = r[r.length - 1];

        // Grab LISTRIGHTS response
        Vector v = new Vector();
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("LISTRIGHTS")) {
                    // listrights_data ::= "LISTRIGHTS" SPACE mailbox
                    // SPACE identifier SPACE rights *(SPACE rights)
                    // read name of mailbox and throw away
                    ir.readAtomString();
                    // read identifier and throw away
                    ir.readAtomString();
                    String rights;
                    while ((rights = ir.readAtomString()) != null)
                        v.addElement(new Rights(rights));
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        Rights[] ra = new Rights[v.size()];
        v.copyInto(ra);
        return ra;
    }

    /**
     * MYRIGHTS Command.
     * 
     * @see "RFC2086"
     */
    public Rights myRights(String mbox) throws ProtocolException {
        if (!hasCapability("ACL"))
            throw new BadCommandException("ACL not supported");

        // encode the mbox as per RFC2060
        mbox = BASE64MailboxEncoder.encode(mbox);

        Argument args = new Argument();
        args.writeString(mbox);

        Response[] r = command("MYRIGHTS", args);
        Response response = r[r.length - 1];

        // Grab MYRIGHTS response
        Rights rights = null;
        if (response.isOK()) { // command succesful
            for (int i = 0, len = r.length; i < len; i++) {
                if (!(r[i] instanceof IMAPResponse))
                    continue;

                IMAPResponse ir = (IMAPResponse) r[i];
                if (ir.keyEquals("MYRIGHTS")) {
                    // myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights
                    // read name of mailbox and throw away
                    ir.readAtomString();
                    String rs = ir.readAtomString();
                    if (rights == null)
                        rights = new Rights(rs);
                    r[i] = null;
                }
            }
        }

        // dispatch remaining untagged responses
        notifyResponseHandlers(r);
        handleResult(response);
        return rights;
    }

    /*
     * The tag used on the IDLE command. Set by idleStart() and
     * used in processIdleResponse() to determine if the response
     * is the matching end tag.
     */
    private String idleTag;

    /**
     * IDLE Command.
     * <p>
     * If the server supports the IDLE command extension, the IDLE command is
     * issued and this method blocks until a response has been received. Once
     * the first response has been received, the IDLE command is terminated and
     * all responses are collected and handled and this method returns.
     * <p>
     * Note that while this method is blocked waiting for a response, no other
     * threads may issue any commands to the server that would use this same
     * connection.
     * 
     * @see "RFC2177"
     * @since JavaMail 1.4.1
     */
    public synchronized void idleStart() throws ProtocolException {
        if (!hasCapability("IDLE"))
            throw new BadCommandException("IDLE not supported");

        Response r;
        // write the command
        try {
            idleTag = writeCommand("IDLE", null);
            r = readResponse();
        } catch (LiteralException lex) {
            r = lex.getResponse();
        } catch (Exception ex) {
            // Convert this into a BYE response
            r = Response.byeResponse(ex);
        }

        if (!r.isContinuation())
            handleResult(r);
    }

    /**
     * While an IDLE command is in progress, read a response
     * sent from the server. The response is read with no locks
     * held so that when the read blocks waiting for the response
     * from the server it's not holding locks that would prevent
     * other threads from interrupting the IDLE command.
     * 
     * @since JavaMail 1.4.1
     */
    public synchronized Response readIdleResponse() {
        if (idleTag == null)
            return null;	// IDLE not in progress
        Response r = null;
        while (r == null) {
            try {
                r = readResponse();
            } catch (InterruptedIOException iioex) {
                /*
                 * If a socket timeout was set, the read will timeout
                 * before the IDLE times out. In that case, just go
                 * back and read some more. After all, the point of
                 * IDLE is to sit here and wait until something happens.
                 */
                if (iioex.bytesTransferred == 0)
                    r = null;	// keep trying
                else
                    // convert this into a BYE response
                    r = Response.byeResponse(iioex);
            } catch (IOException ioex) {
                // convert this into a BYE response
                r = Response.byeResponse(ioex);
            } catch (ProtocolException pex) {
                // convert this into a BYE response
                r = Response.byeResponse(pex);
            }
        }
        return r;
    }

    /**
     * Process a response returned by readIdleResponse().
     * This method will be called with appropriate locks
     * held so that the processing of the response is safe.
     * 
     * @since JavaMail 1.4.1
     */
    public boolean processIdleResponse(Response r) throws ProtocolException {
        Response[] responses = new Response[1];
        responses[0] = r;
        boolean done = false;		// done reading responses?
        notifyResponseHandlers(responses);

        if (r.isBYE()) // shouldn't wait for command completion response
            done = true;

        // If this is a matching command completion response, we are done
        if (r.isTagged() && r.getTag().equals(idleTag))
            done = true;

        if (done)
            idleTag = null;	// no longer in IDLE

        handleResult(r);
        return !done;
    }

    // the DONE command to break out of IDLE
    private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' };

    /**
     * Abort an IDLE command. While one thread is blocked in
     * readIdleResponse(), another thread will use this method
     * to abort the IDLE command, which will cause the server
     * to send the closing tag for the IDLE command, which
     * readIdleResponse() and processIdleResponse() will see
     * and terminate the IDLE state.
     * 
     * @since JavaMail 1.4.1
     */
    public void idleAbort() throws ProtocolException {
        OutputStream os = getOutputStream();
        try {
            os.write(DONE);
            os.flush();
        } catch (IOException ex) {
            // nothing to do, hope to detect it again later
        }
    }
}
